<?php
declare(strict_types=1);

namespace WebApp\lib\db\mongo;

use Closure;
use ErrorException;
use MongoDB\Driver\BulkWrite;
use MongoDB\Driver\Command;
use MongoDB\Driver\Exception\Exception;
use MongoDB\Driver\Manager;
use MongoDB\Driver\Query;
use Throwable;
use WebApp\lib\db\IDriver;

/**
 * Class Table
 * @package txz\common\third_lib\model\lib
 */
class MongoDriver implements IDriver
{
    /**
     * @var IMongoConfig
     */
    private $config;

    public function __construct(IMongoConfig $config)
    {
        $this->config = $config;
    }

    public function getCon()
    {
        $host = $this->config->getHost();
        $port = $this->config->getPort();
//        var_dump("mongodb://$host:$port");exit;
        $client = new Manager("mongodb://$host:$port");
        return $client;
    }

    /**
     * @param MongoTable $model
     * @param array $data
     * @return Closure
     */
    public function insert(MongoTable $model, array $data)
    {
        return function (Manager $client)use($model, $data) {
            $bulk = new BulkWrite;
            $bulk->insert($data);
            return $this->execute($client, $model, $bulk);
        };
    }

    /**
     * @param MongoTable $model
     * @param $data
     * @param array $where
     * @param bool $multi
     * @param bool $upsert
     * @param string $opt
     * @return Closure
     */
    public function update(MongoTable $model, $data, $where = [], $multi=false, $upsert=false, $opt = '$set')
    {
        return function(Manager $client) use($model, $data, $where, $multi, $upsert, $opt) {
            $bulk = new BulkWrite;
            $bulk->update(
                $where,
                [$opt => $data],
                ['multi' => $multi, 'upsert' => $upsert]
            );
            return $this->execute($client, $model, $bulk);
        };
    }

    /**
     * @param Manager $client
     * @param BulkWrite $bulk
     * @param MongoTable $model
     * @return bool
     */
    private function execute(Manager $client, MongoTable $model, BulkWrite $bulk): bool
    {
        try{
            $res = $client->executeBulkWrite($model->getDbName() . "." . $model->getTableName(), $bulk, null);
            if($res->getWriteConcernError()){
                throw new ErrorException($res->getWriteConcernError());
            }else{
                if ($res->getWriteErrors()){
                    throw new ErrorException($res->getWriteErrors());
                }else{
                    return true;
                }
            }
        }catch (ErrorException $e){
            tracker(E, $e->getMessage());
            return false;
        }
    }

    /**
     * @param MongoTable $model
     * @param array $filter
     * @param int $limit
     * @return Closure
     */
    public function remove(MongoTable $model, array $filter, int $limit = 0)
    {
        return function(Manager $client) use($model, $filter, $limit) {
            $bulk = new BulkWrite;
            if ($limit <= 0) {
                $bulk->delete($filter);
            } else {
                $bulk->delete($filter, ['limit' => $limit]);
            }
            return $this->execute($client, $model, $bulk);
        };
    }

    /**
     * @param MongoTable $model
     * @param array $filter
     * @param array $options
     * @return Closure
     */
    public function getObj(MongoTable $model, $filter = [], $options = [])
    {
        return function(Manager $client) use($model, $filter, $options) {
            $res = $this->findAll($client, $model, $filter, $options, 1);
            if (is_array($res) && $res) {
                $obj = $res[0];
                return $obj;
            } else {
                return $res;
            }
        };
    }

    /**
     * @param MongoTable $model
     * @param array $filter
     * @param array $options
     * @return Closure
     */
    public function getObjArr(MongoTable $model, $filter = [], $options = [])
    {
        return function(Manager $client) use($model, $filter, $options) {
            $res = $this->findAll($client, $model, $filter, $options);
            if (is_array($res) && $res) {
                return $res;
            } else {
                return $res;
            }
        };
    }

    /**
     * @param Manager $client
     * @param MongoTable $model
     * @param array $filter
     * @param array $options
     * @param null $limit
     * @param null $offset
     * @return array|bool
     */
    public function findAll(Manager $client, MongoTable $model, $filter = [], $options = [], $limit = null, $offset = null)
    {
        if (is_numeric($limit)) {
            $options['limit'] = $limit;
        }

        if (is_numeric($offset)) {
            $options['skip'] = $offset;
        }
//tracker(I, $filter);
//tracker(I, $options);

        $query = new Query($filter, $options);
        try {
            $cursor = $client->executeQuery($model->getDbName() . "." . $model->getTableName(), $query);
        } catch (Exception $e) {
            tracker(E, $e);
            return false;
        }
        if ($cursor) {
            return $cursor->toArray();
//            if ($is_array) {
//                return $this->object_to_array($cursor->toArray());
//            } else {
//                return $cursor->toArray();
//            }
        }
        return false;
    }

    private function executeCommand(Manager $client, MongoTable $model, $command){
        try {
            $cursor = $client->executeCommand($model->getDbName(), new Command($command));
            if ($cursor) {
                return $cursor->toArray();
            }
            return false;
        } catch (Exception $e) {
            return false;
        }
    }

    public function aggregate(Manager $client, MongoTable $model, array $pipeline, array $options = [])
    {
        $command = [
                'aggregate' => $model->getTableName(),
                'pipeline'  => $pipeline
            ] + $options;
        return $this->executeCommand($client, $model, $command);
    }

    /**
     * @param $conn
     * @param Throwable $e
     */
    public function catchErr(&$conn, Throwable $e)
    {
        tracker(E, $e->getTraceAsString());
    }

    public function getTryCount(): int
    {
        return $this->config->getTryCount();
    }
}
